فارسی

با هوک useReducer در React برای مدیریت مؤثر حالت‌های پیچیده برنامه آشنا شوید و کارایی و قابلیت نگهداری پروژه‌های جهانی React را بهبود بخشید.

الگوی useReducer در React: تسلط بر مدیریت حالت‌های پیچیده

در چشم‌انداز همواره در حال تحول توسعه فرانت‌اند، React خود را به عنوان یک فریمورک پیشرو برای ساخت رابط‌های کاربری تثبیت کرده است. با افزایش پیچیدگی برنامه‌ها، مدیریت حالت (state) به طور فزاینده‌ای چالش‌برانگیز می‌شود. هوک useState راه ساده‌ای برای مدیریت حالت در یک کامپوننت فراهم می‌کند، اما برای سناریوهای پیچیده‌تر، React جایگزین قدرتمندی ارائه می‌دهد: هوک useReducer. این پست وبلاگ به بررسی الگوی useReducer می‌پردازد و مزایا، پیاده‌سازی‌های عملی و چگونگی بهبود چشمگیر برنامه‌های React شما در سطح جهانی را بررسی می‌کند.

درک نیاز به مدیریت حالت پیچیده

هنگام ساخت برنامه‌های React، ما اغلب با موقعیت‌هایی مواجه می‌شویم که در آن حالت یک کامپوننت صرفاً یک مقدار ساده نیست، بلکه مجموعه‌ای از داده‌های به هم پیوسته یا حالتی است که به مقادیر حالت قبلی بستگی دارد. این مثال‌ها را در نظر بگیرید:

در این سناریوها، استفاده از useState به تنهایی می‌تواند به کدی پیچیده و دشوار برای مدیریت منجر شود. به‌روزرسانی چندین متغیر حالت در پاسخ به یک رویداد واحد می‌تواند خسته‌کننده شود و منطق مدیریت این به‌روزرسانی‌ها ممکن است در سراسر کامپوننت پراکنده شود، که درک و نگهداری آن را دشوار می‌کند. اینجاست که useReducer می‌درخشد.

معرفی هوک useReducer

هوک useReducer جایگزینی برای useState برای مدیریت منطق حالت پیچیده است. این هوک بر اساس اصول الگوی Redux ساخته شده است، اما در خود کامپوننت React پیاده‌سازی می‌شود و در بسیاری از موارد نیاز به یک کتابخانه خارجی جداگانه را از بین می‌برد. این هوک به شما امکان می‌دهد منطق به‌روزرسانی حالت خود را در یک تابع واحد به نام reducer متمرکز کنید.

هوک useReducer دو آرگومان می‌گیرد:

این هوک یک آرایه حاوی دو عنصر را برمی‌گرداند:

تابع Reducer

تابع reducer قلب الگوی useReducer است. این یک تابع خالص است، به این معنی که نباید هیچ اثر جانبی (side effect) داشته باشد (مانند فراخوانی API یا تغییر متغیرهای سراسری) و باید همیشه برای ورودی یکسان، خروجی یکسانی را برگرداند. تابع reducer دو آرگومان می‌گیرد:

در داخل تابع reducer، شما از یک دستور switch یا دستورات if/else if برای مدیریت انواع مختلف اکشن و به‌روزرسانی حالت بر اساس آن استفاده می‌کنید. این کار منطق به‌روزرسانی حالت شما را متمرکز می‌کند و استدلال در مورد چگونگی تغییر حالت در پاسخ به رویدادهای مختلف را آسان‌تر می‌سازد.

تابع Dispatch

تابع dispatch روشی است که شما برای فعال کردن به‌روزرسانی‌های حالت استفاده می‌کنید. وقتی شما dispatch(action) را فراخوانی می‌کنید، اکشن به تابع reducer ارسال می‌شود که سپس حالت را بر اساس نوع و payload اکشن به‌روزرسانی می‌کند.

یک مثال عملی: پیاده‌سازی یک شمارنده

بیایید با یک مثال ساده شروع کنیم: یک کامپوننت شمارنده. این مثال مفاهیم اصلی را قبل از رفتن به مثال‌های پیچیده‌تر نشان می‌دهد. ما یک شمارنده خواهیم ساخت که می‌تواند افزایش، کاهش و بازنشانی (reset) شود:


import React, { useReducer } from 'react';

// تعریف انواع اکشن
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';

// تعریف تابع reducer
function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case RESET:
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  // مقداردهی اولیه useReducer
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>تعداد: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>افزایش</button>
      <button onClick={() => dispatch({ type: DECREMENT })}>کاهش</button>
      <button onClick={() => dispatch({ type: RESET })}>بازنشانی</button>
    </div>
  );
}

export default Counter;

در این مثال:

توسعه مثال شمارنده: افزودن Payload

بیایید شمارنده را طوری تغییر دهیم که اجازه دهد با یک مقدار مشخص افزایش یابد. این کار مفهوم payload را در یک اکشن معرفی می‌کند:


import React, { useReducer } from 'react';

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_VALUE = 'SET_VALUE';

function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + action.payload };
    case DECREMENT:
      return { count: state.count - action.payload };
    case RESET:
      return { count: 0 };
    case SET_VALUE:
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  const [inputValue, setInputValue] = React.useState(1);

  return (
    <div>
      <p>تعداد: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT, payload: parseInt(inputValue) || 1 })}>افزایش به اندازه {inputValue}</button>
      <button onClick={() => dispatch({ type: DECREMENT, payload: parseInt(inputValue) || 1 })}>کاهش به اندازه {inputValue}</button>
      <button onClick={() => dispatch({ type: RESET })}>بازنشانی</button>
       <input
         type="number"
         value={inputValue}
         onChange={(e) => setInputValue(e.target.value)}
       />
      </div>
  );
}

export default Counter;

در این مثال گسترش‌یافته:

مزایای استفاده از useReducer

الگوی useReducer چندین مزیت نسبت به استفاده مستقیم از useState برای مدیریت حالت پیچیده ارائه می‌دهد:

چه زمانی از useReducer استفاده کنیم

در حالی که useReducer مزایای قابل توجهی ارائه می‌دهد، همیشه انتخاب درستی نیست. استفاده از useReducer را در موارد زیر در نظر بگیرید:

برای به‌روزرسانی‌های ساده حالت، useState اغلب کافی و ساده‌تر برای استفاده است. هنگام تصمیم‌گیری، پیچیدگی حالت خود و پتانسیل رشد آن را در نظر بگیرید.

مفاهیم و تکنیک‌های پیشرفته

ترکیب useReducer با Context

برای مدیریت حالت سراسری یا به اشتراک‌گذاری حالت بین چندین کامپوننت، می‌توانید useReducer را با Context API ری‌اکت ترکیب کنید. این رویکرد اغلب برای پروژه‌های کوچک تا متوسط که نمی‌خواهید وابستگی‌های اضافی معرفی کنید، به Redux ترجیح داده می‌شود.


import React, { createContext, useReducer, useContext } from 'react';

// تعریف انواع اکشن و reducer (مانند قبل)
const INCREMENT = 'INCREMENT';
// ... (سایر انواع اکشن و تابع counterReducer)

const CounterContext = createContext();

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

function useCounter() {
  return useContext(CounterContext);
}

function Counter() {
  const { state, dispatch } = useCounter();

  return (
    <div>
      <p>تعداد: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>افزایش</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

export default App;

در این مثال:

تست کردن useReducer

تست کردن reducerها ساده است زیرا آن‌ها توابع خالص هستند. شما می‌توانید به راحتی تابع reducer را به صورت جداگانه با استفاده از یک فریمورک تست واحد مانند Jest یا Mocha تست کنید. در اینجا یک مثال با استفاده از Jest آورده شده است:


import { counterReducer } from './counterReducer'; // با فرض اینکه counterReducer در یک فایل جداگانه قرار دارد

const INCREMENT = 'INCREMENT';

describe('counterReducer', () => {
  it('should increment the count', () => {
    const state = { count: 0 };
    const action = { type: INCREMENT };
    const newState = counterReducer(state, action);
    expect(newState.count).toBe(1);
  });

   it('should return the same state for unknown action types', () => {
        const state = { count: 10 };
        const action = { type: 'UNKNOWN_ACTION' };
        const newState = counterReducer(state, action);
        expect(newState).toBe(state); // اطمینان از اینکه حالت تغییر نکرده است
    });
});

تست کردن reducerهای شما تضمین می‌کند که آن‌ها همانطور که انتظار می‌رود رفتار می‌کنند و بازسازی منطق حالت شما را آسان‌تر می‌کند. این یک گام حیاتی در ساخت برنامه‌های قوی و قابل نگهداری است.

بهینه‌سازی عملکرد با Memoization

هنگام کار با حالت‌های پیچیده و به‌روزرسانی‌های مکرر، استفاده از useMemo را برای بهینه‌سازی عملکرد کامپوننت‌های خود در نظر بگیرید، به خصوص اگر مقادیر مشتق شده‌ای دارید که بر اساس حالت محاسبه می‌شوند. به عنوان مثال:


import React, { useReducer, useMemo } from 'react';

function reducer(state, action) {
  // ... (منطق reducer) 
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  // محاسبه یک مقدار مشتق شده، و memoize کردن آن با useMemo
  const derivedValue = useMemo(() => {
    // محاسبه سنگین بر اساس حالت
    return state.value1 + state.value2;
  }, [state.value1, state.value2]); // وابستگی‌ها: فقط زمانی که این مقادیر تغییر می‌کنند دوباره محاسبه شود

  return (
    <div>
      <p>مقدار مشتق شده: {derivedValue}</p>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE1', payload: 10 })}>به‌روزرسانی مقدار ۱</button>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE2', payload: 20 })}>به‌روزرسانی مقدار ۲</button>
    </div>
  );
}

در این مثال، derivedValue فقط زمانی محاسبه می‌شود که state.value1 یا state.value2 تغییر کنند، که از محاسبات غیرضروری در هر رندر مجدد جلوگیری می‌کند. این رویکرد یک روش معمول برای اطمینان از عملکرد بهینه رندر است.

مثال‌ها و موارد استفاده در دنیای واقعی

بیایید چند مثال عملی از جایی که useReducer یک ابزار ارزشمند در ساخت برنامه‌های React برای مخاطبان جهانی است را بررسی کنیم. توجه داشته باشید که این مثال‌ها برای نشان دادن مفاهیم اصلی ساده‌سازی شده‌اند. پیاده‌سازی‌های واقعی ممکن است شامل منطق و وابستگی‌های پیچیده‌تری باشند.

۱. فیلترهای محصولات در فروشگاه آنلاین

یک وب‌سایت فروشگاه آنلاین (مانند پلتفرم‌های محبوبی چون آمازون یا علی‌اکسپرس که در سطح جهانی در دسترس هستند) را با کاتالوگ محصولات بزرگ تصور کنید. کاربران نیاز دارند محصولات را بر اساس معیارهای مختلفی (محدوده قیمت، برند، اندازه، رنگ، کشور مبدأ و غیره) فیلتر کنند. useReducer برای مدیریت حالت فیلتر ایده‌آل است.


import React, { useReducer } from 'react';

const initialState = {
  priceRange: { min: 0, max: 1000 },
  brand: [], // آرایه‌ای از برندهای انتخاب شده
  color: [], // آرایه‌ای از رنگ‌های انتخاب شده
  //... سایر معیارهای فیلتر
};

function filterReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_PRICE_RANGE':
      return { ...state, priceRange: action.payload };
    case 'TOGGLE_BRAND':
      const brand = action.payload;
      return { ...state, brand: state.brand.includes(brand) ? state.brand.filter(b => b !== brand) : [...state.brand, brand] };
    case 'TOGGLE_COLOR':
      // منطق مشابه برای فیلتر کردن رنگ
      return { ...state, color: state.color.includes(action.payload) ? state.color.filter(c => c !== action.payload) : [...state.color, action.payload] };
    // ... سایر اکشن‌های فیلتر
    default:
      return state;
  }
}

function ProductFilter() {
  const [state, dispatch] = useReducer(filterReducer, initialState);

  // کامپوننت‌های UI برای انتخاب معیارهای فیلتر و فعال کردن اکشن‌های dispatch
  // به عنوان مثال: ورودی بازه برای قیمت، چک‌باکس‌ها برای برندها و غیره.

  return (
    <div>
      <!-- عناصر UI فیلتر -->
    </div>
  );
}

این مثال نشان می‌دهد که چگونه می‌توان چندین معیار فیلتر را به صورت کنترل‌شده مدیریت کرد. وقتی کاربر هر یک از تنظیمات فیلتر (قیمت، برند و غیره) را تغییر می‌دهد، reducer حالت فیلتر را بر اساس آن به‌روزرسانی می‌کند. سپس کامپوننتی که مسئول نمایش محصولات است، از حالت به‌روزرسانی‌شده برای فیلتر کردن محصولات نمایش داده شده استفاده می‌کند. این الگو از ساخت سیستم‌های فیلترینگ پیچیده که در پلتفرم‌های تجارت الکترونیک جهانی رایج است، پشتیبانی می‌کند.

۲. فرم‌های چند مرحله‌ای (مثلاً فرم‌های ارسال بین‌المللی)

بسیاری از برنامه‌ها شامل فرم‌های چند مرحله‌ای هستند، مانند فرم‌هایی که برای ارسال بین‌المللی یا ایجاد حساب‌های کاربری با الزامات پیچیده استفاده می‌شوند. useReducer در مدیریت حالت چنین فرم‌هایی عالی عمل می‌کند.


import React, { useReducer } from 'react';

const initialState = {
  step: 1, // مرحله فعلی در فرم
  formData: {
    firstName: '',
    lastName: '',
    address: '',
    city: '',
    country: '',
    // ... سایر فیلدهای فرم
  },
  errors: {},
};

function formReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREV_STEP':
      return { ...state, step: state.step - 1 };
    case 'UPDATE_FIELD':
      return { ...state, formData: { ...state.formData, [action.payload.field]: action.payload.value } };
    case 'SET_ERRORS':
      return { ...state, errors: action.payload };
    case 'SUBMIT_FORM':
      // منطق ارسال فرم را اینجا مدیریت کنید، به عنوان مثال، فراخوانی‌های API
      return state;
    default:
      return state;
  }
}

function MultiStepForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  // منطق رندر برای هر مرحله از فرم
  // بر اساس مرحله فعلی در حالت
  const renderStep = () => {
    switch (state.step) {
      case 1:
        return <Step1 formData={state.formData} dispatch={dispatch} />;
      case 2:
        return <Step2 formData={state.formData} dispatch={dispatch} />;
      // ... سایر مراحل
      default:
        return <p>مرحله نامعتبر</p>;
    }
  };

  return (
    <div>
      {renderStep()}
      <!-- دکمه‌های ناوبری (بعدی، قبلی، ارسال) بر اساس مرحله فعلی -->
    </div>
  );
}

این مثال نشان می‌دهد که چگونه می‌توان فیلدهای مختلف فرم، مراحل و خطاهای اعتبارسنجی احتمالی را به روشی ساختاریافته و قابل نگهداری مدیریت کرد. این امر برای ساخت فرآیندهای ثبت‌نام یا پرداخت کاربرپسند، به ویژه برای کاربران بین‌المللی که ممکن است انتظارات متفاوتی بر اساس آداب و رسوم محلی و تجربه خود با پلتفرم‌های مختلفی مانند فیسبوک یا وی‌چت داشته باشند، حیاتی است.

۳. برنامه‌های بی‌درنگ (چت، ابزارهای همکاری)

useReducer برای برنامه‌های بی‌درنگ (real-time)، مانند ابزارهای همکاری مانند Google Docs یا برنامه‌های پیام‌رسان، مفید است. این هوک رویدادهایی مانند دریافت پیام، پیوستن/ترک کردن کاربر و وضعیت اتصال را مدیریت می‌کند و اطمینان می‌دهد که UI به صورت لازم به‌روزرسانی می‌شود.


import React, { useReducer, useEffect } from 'react';

const initialState = {
  messages: [],
  users: [],
  connectionStatus: 'connecting',
};

function chatReducer(state, action) {
  switch (action.type) {
    case 'RECEIVE_MESSAGE':
      return { ...state, messages: [...state.messages, action.payload] };
    case 'USER_JOINED':
      return { ...state, users: [...state.users, action.payload] };
    case 'USER_LEFT':
      return { ...state, users: state.users.filter(user => user.id !== action.payload.id) };
    case 'SET_CONNECTION_STATUS':
      return { ...state, connectionStatus: action.payload };
    default:
      return state;
  }
}

function ChatRoom() {
  const [state, dispatch] = useReducer(chatReducer, initialState);

  useEffect(() => {
    // برقراری اتصال WebSocket (مثال):
    const socket = new WebSocket('wss://your-websocket-server.com');

    socket.onopen = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
    socket.onmessage = (event) => dispatch({ type: 'RECEIVE_MESSAGE', payload: JSON.parse(event.data) });
    socket.onclose = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });

    return () => socket.close(); // پاکسازی هنگام unmount شدن کامپوننت
  }, []);

  // رندر پیام‌ها، لیست کاربران و وضعیت اتصال بر اساس حالت
  return (
    <div>
      <p>وضعیت اتصال: {state.connectionStatus}</p>
      <!-- UI برای نمایش پیام‌ها، لیست کاربران و ارسال پیام‌ها -->
    </div>
  );
}

این مثال پایه‌ای برای مدیریت یک چت بی‌درنگ فراهم می‌کند. حالت، ذخیره‌سازی پیام‌ها، کاربران حاضر در چت و وضعیت اتصال را مدیریت می‌کند. هوک useEffect مسئول برقراری اتصال WebSocket و مدیریت پیام‌های ورودی است. این رویکرد یک رابط کاربری واکنش‌گرا و پویا ایجاد می‌کند که به کاربران در سراسر جهان خدمات می‌دهد.

بهترین شیوه‌ها برای استفاده از useReducer

برای استفاده مؤثر از useReducer و ایجاد برنامه‌های قابل نگهداری، این بهترین شیوه‌ها را در نظر بگیرید:

نتیجه‌گیری

هوک useReducer یک ابزار قدرتمند و همه‌کاره برای مدیریت حالت پیچیده در برنامه‌های React است. این هوک مزایای متعددی از جمله منطق حالت متمرکز، سازماندهی بهتر کد و قابلیت تست‌پذیری بهبودیافته را ارائه می‌دهد. با پیروی از بهترین شیوه‌ها و درک مفاهیم اصلی آن، می‌توانید از useReducer برای ساخت برنامه‌های React قوی‌تر، قابل نگهداری‌تر و با عملکرد بهتر استفاده کنید. این الگو به شما قدرت می‌دهد تا چالش‌های مدیریت حالت پیچیده را به طور مؤثر برطرف کنید و به شما امکان می‌دهد برنامه‌های آماده برای بازار جهانی بسازید که تجربیات کاربری یکپارچه‌ای را در سراسر جهان ارائه می‌دهند.

هرچه عمیق‌تر در توسعه React پیش می‌روید، گنجاندن الگوی useReducer در جعبه ابزار شما بدون شک به پایگاه‌های کد تمیزتر، مقیاس‌پذیرتر و با قابلیت نگهداری آسان‌تر منجر خواهد شد. به یاد داشته باشید که همیشه نیازهای خاص برنامه خود را در نظر بگیرید و بهترین رویکرد را برای مدیریت حالت برای هر موقعیت انتخاب کنید. کدنویسی خوش!